/*
 * Copyright 2012 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.dspl.xml;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.dspl.jaxb.AttributeXml;
import com.google.dspl.jaxb.ConceptPropertyXml;
import com.google.dspl.jaxb.ConceptTableMappingXml;
import com.google.dspl.jaxb.ConceptTableMappingXml.MapProperty;
import com.google.dspl.jaxb.ConceptXml;
import com.google.dspl.jaxb.DataTypeXml;
import com.google.dspl.jaxb.DataXml;
import com.google.dspl.jaxb.Dspl;
import com.google.dspl.jaxb.InfoXml;
import com.google.dspl.jaxb.SliceConceptRefXml;
import com.google.dspl.jaxb.SliceTableMappingXml;
import com.google.dspl.jaxb.SliceTableMappingXml.MapDimension;
import com.google.dspl.jaxb.SliceTableMappingXml.MapMetric;
import com.google.dspl.jaxb.SliceXml;
import com.google.dspl.jaxb.TableXml;
import com.google.dspl.jaxb.TopicXml;
import com.google.dspl.jaxb.ValueXml;
import com.google.dspl.model.Attribute;
import com.google.dspl.model.Concept;
import com.google.dspl.model.Concept.ConceptProperty;
import com.google.dspl.model.DataType;
import com.google.dspl.model.Dataset;
import com.google.dspl.model.Identifier;
import com.google.dspl.model.Info;
import com.google.dspl.model.Slice;
import com.google.dspl.model.Table;
import com.google.dspl.model.Topic;
import com.google.dspl.model.Value;
import com.google.dspl.validate.ConceptInheritenceTraverser;
import com.google.dspl.validate.ConceptInheritenceTraverser.ConceptNode;
import com.google.dspl.validate.DatasetResolver;

import java.util.List;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.namespace.QName;

/**
 * Conversion of a JAXB {@link Dspl} object to a {@link Dataset}.
 *
 * @author Shardul Deo
 */
class DsplToDatasetConversion {

  private final DatasetResolver datasetResolver;
  private final NamespaceMapping namespaceMapping;
  private final Dspl dspl;
  private final String datasetId;

  /**
   * @param datasetResolver Used for resolving references to external concepts.
   * @param namespaceMapping Mapping between dataset namespaces and their ids. If the DSPL being
   *        converted contains references to external namespaces, this map must contain a mapping
   *        for them.
   * @param dspl The DSPL that needs to be converted
   * @param datasetId The id of the resulting dataset
   */
  DsplToDatasetConversion(DatasetResolver datasetResolver, NamespaceMapping namespaceMapping,
      Dspl dspl, String datasetId) {
    this.datasetResolver = Preconditions.checkNotNull(datasetResolver);
    this.namespaceMapping = Preconditions.checkNotNull(namespaceMapping);
    this.dspl = Preconditions.checkNotNull(dspl);
    this.datasetId = Preconditions.checkNotNull(datasetId);
  }

  Dataset convert() {
    Dataset dataset = new Dataset();
    dataset.setDatasetId(datasetId);
    if (dspl.getInfo() != null) {
      dataset.setInfo(convertInfo(dspl.getInfo()));
    }
    if (dspl.getProvider() != null) {
      dataset.setProvider(convertInfo(dspl.getProvider()));
    }
    for (AttributeXml attribute : dspl.getAttribute()) {
      dataset.addAttribute(convertAttribute(attribute, null));
    }
    if (dspl.getTopics() != null) {
      for (TopicXml topic : dspl.getTopics().getTopic()) {
        dataset.addTopic(convertTopic(topic));
      }
    }
    if (dspl.getConcepts() != null) {
      for (ConceptXml concept : dspl.getConcepts().getConcept()) {
        dataset.addConcept(convertConcept(concept));
      }
    }
    if (dspl.getSlices() != null) {
      for (SliceXml slice : dspl.getSlices().getSlice()) {
        dataset.addSlice(convertSlice(slice));
      }
    }
    if (dspl.getTables() != null) {
      for (TableXml table : dspl.getTables().getTable()) {
        dataset.addTable(convertTable(table));
      }
    }
    return dataset;
  }

  private Topic convertTopic(TopicXml topicXml) {
    Topic topic = new Topic();
    topic.setId(topicXml.getId());
    if (topicXml.getInfo() != null) {
      topic.setInfo(convertInfo(topicXml.getInfo()));
    }
    if (topicXml.getParentTopic() != null) {
      topic.setParentTopic(convertQName(topicXml.getParentTopic()));
    }
    for (TopicXml child : topicXml.getTopic()) {
      topic.addChildTopic(convertTopic(child));
    }
    return topic;
  }

  private Concept convertConcept(ConceptXml conceptXml) {
    Concept concept = new Concept();
    concept.setId(conceptXml.getId());
    if (conceptXml.getExtends() != null) {
      concept.setParent(convertQName(conceptXml.getExtends()));
    }
    if (conceptXml.getInfo() != null) {
      // TODO: Add plural, total etc. names.
      concept.setInfo(convertInfo(conceptXml.getInfo()));
    }
    for (ConceptXml.Topic topic : conceptXml.getTopic()) {
      concept.addTopic(convertQName(topic.getRef()));
    }
    if (conceptXml.getType() != null) {
      concept.setType(convertType(conceptXml.getType().getRef()));
    }
    for (AttributeXml attributeXml : conceptXml.getAttribute()) {
      concept.addAttribute(convertAttribute(attributeXml, conceptXml));
    }
    for (ConceptPropertyXml property : conceptXml.getProperty()) {
      concept.addProperty(convertProperty(property));
    }
    if (conceptXml.getDefaultValue() != null) {
      // Get correct type, possibly inferred from inheritance
      DataType dataType = ConceptInheritenceTraverser.resolveConceptType(
          new ConceptNodeImpl(conceptXml));
      if (dataType == null) {
        throw new IllegalArgumentException("Type must be specified");
      }
      concept.setDefaultValue(convertValue(conceptXml.getDefaultValue().getValue(), dataType));
    }
    if (conceptXml.getTable() != null) {
      ConceptTableMappingXml tableXml = conceptXml.getTable();
      Concept.ConceptTableMapping tableMapping = new Concept.ConceptTableMapping();
      if (tableXml.getMapConcept() != null) {
        tableMapping.setConceptMappingColumn(tableXml.getMapConcept().getToColumn());
      }
      for (MapProperty mapProperty : tableXml.getMapProperty()) {
        Concept.PropertyMapping propertyMapping = new Concept.PropertyMapping();
        propertyMapping.setPropertyId(mapProperty.getRef());
        propertyMapping.setToColumn(mapProperty.getToColumn());
        if (mapProperty.getLang() != null) {
          propertyMapping.setLanguage(mapProperty.getLang());
        }
        tableMapping.addPropertyMapping(propertyMapping);
      }
      tableMapping.setTableId(tableXml.getRef());
      concept.setTable(tableMapping);
    }
    return concept;
  }

  private ConceptProperty convertProperty(ConceptPropertyXml propertyXml) {
    ConceptProperty conceptProperty = new ConceptProperty();
    if (propertyXml.getId() != null) {
      conceptProperty.setId(propertyXml.getId());
    }
    if (propertyXml.getInfo() != null) {
      conceptProperty.setInfo(convertInfo(propertyXml.getInfo()));
    }
    if (propertyXml.getType() != null) {
      conceptProperty.setType(convertType(propertyXml.getType().getRef()));
    }
    if (propertyXml.getConcept() != null) {
      conceptProperty.setConcept(convertQName(propertyXml.getConcept()));
    }
    // TODO: Only set this if explicitly set
    conceptProperty.setIsParent(propertyXml.isIsParent());
    conceptProperty.setIsMapping(propertyXml.isIsMapping());
    conceptProperty.setIsRequired(propertyXml.isIsRequired());
    return conceptProperty;
  }

  private Slice convertSlice(SliceXml sliceXml) {
    Slice slice = new Slice();
    slice.setId(sliceXml.getId());
    if (sliceXml.getInfo() != null) {
      slice.setInfo(convertInfo(sliceXml.getInfo()));
    }
    for (SliceConceptRefXml dimension : sliceXml.getDimension()) {
     slice.addDimension(convertQName(dimension.getConcept()));
    }
    for (SliceConceptRefXml metric : sliceXml.getMetric()) {
      slice.addMetric(convertQName(metric.getConcept()));
     }
    if (sliceXml.getTable() != null) {
      Slice.TableMapping tableMapping = new Slice.TableMapping();
      SliceTableMappingXml tableXml = sliceXml.getTable();
      for (MapDimension mapDimension :  tableXml.getMapDimension()) {
        Slice.ConceptMapping conceptMapping = new Slice.ConceptMapping();
        conceptMapping.setConcept(convertQName(mapDimension.getConcept()));
        conceptMapping.setToColumn(mapDimension.getToColumn());
        tableMapping.addDimensionMapping(conceptMapping);
      }
      for (MapMetric mapMetric :  tableXml.getMapMetric()) {
        Slice.ConceptMapping conceptMapping = new Slice.ConceptMapping();
        conceptMapping.setConcept(convertQName(mapMetric.getConcept()));
        conceptMapping.setToColumn(mapMetric.getToColumn());
        tableMapping.addMetricMapping(conceptMapping);
      }
      tableMapping.setTableId(tableXml.getRef().getLocalPart());
      slice.setTable(tableMapping);
    }
    return slice;
  }

  private Table convertTable(TableXml tableXml) {
    Table table = new Table();
    table.setId(tableXml.getId());
    if (tableXml.getInfo() != null) {
      table.setInfo(convertInfo(tableXml.getInfo()));
    }
    for (TableXml.Column columnXml : tableXml.getColumn()) {
      Table.Column column = new Table.Column();
      column.setId(columnXml.getId());
      if (columnXml.getType() != null) {
        column.setType(convertType(columnXml.getType()));
      }
      if (columnXml.getValue() != null) {
        column.setConstantValue(convertValue(columnXml.getValue(), convertType(
            columnXml.getType())));
      }
      if (columnXml.getFormat() != null) {
        column.setFormat(columnXml.getFormat());
      }
      table.addColumn(column);
    }
    if (tableXml.getData() != null) {
      DataXml.File fileXml = tableXml.getData().getFile();
      Table.Data data = new Table.Data();
      if (fileXml.getFormat() != null) {
        data.setFormat(fileXml.getFormat());
      }
      if (fileXml.getEncoding() != null) {
        data.setEncoding(fileXml.getEncoding());
      }
      data.setFile(fileXml.getValue());
      table.setData(data);
    }
    return table;
  }

  @VisibleForTesting
  Attribute convertAttribute(AttributeXml attributeXml, ConceptXml enclosingConcept) {
    Attribute attribute = new Attribute();
    if (attributeXml.getId() != null) {
      attribute.setId(attributeXml.getId());
    }
    if (attributeXml.getConcept() != null) {
      attribute.setConcept(convertQName(attributeXml.getConcept()));
    }
    if (attributeXml.getInfo() != null) {
      attribute.setInfo(convertInfo(attributeXml.getInfo()));
    }
    if (attributeXml.getType() != null) {
      attribute.setType(convertType(attributeXml.getType().getRef()));
    }
    List<ValueXml> values = attributeXml.getValue();
    if (!values.isEmpty()) {
      if (values.size() > 1 || !Strings.isNullOrEmpty(values.get(0).getLang())) {
        // Multiple values or language implies string values
        // TODO: Would we ever have language set for non string values?
        attribute.setStringValues(convertStringValues(values));
      } else {
        DataType dataType = attribute.getType();
        if (dataType == null && enclosingConcept != null) {
          // Try inferring it by inheritance
          String attributeId = attributeXml.getId() != null
              ? attributeXml.getId() : attributeXml.getConcept().getLocalPart();
          dataType = ConceptInheritenceTraverser.resolveConceptAttributeType(
              attributeId, new ConceptNodeImpl(enclosingConcept));
        }
        if (dataType == null) {
          throw new IllegalArgumentException("Type must be specified");
        }
        attribute.setValue(convertValue(values.get(0).getValue(), dataType));
      }
    }
    if (attributeXml.getConceptValue() != null) {
      Value value = new Value();
      value.setConceptValue(convertQName(attributeXml.getConceptValue().getConcept()));
      attribute.setValue(value);
    }
    return attribute;
  }

  @VisibleForTesting
  Value convertValue(String valueXml, DataType dataType) {
    Value value = new Value();
    switch (dataType) {
      case STRING:
        value.setStringValue(valueXml);
        break;
      case FLOAT:
        value.setFloatValue(Double.valueOf(valueXml));
        break;
      case INTEGER:
        value.setIntegerValue(Long.valueOf(valueXml));
        break;
      case BOOLEAN:
        value.setBooleanValue(Boolean.valueOf(valueXml));
        break;
      case DATE:
        // TODO: Use format to parse
        value.setDateValue(Long.valueOf(valueXml));
        break;
      case CONCEPT:
        throw new IllegalArgumentException("Specify as concept value instead of primitive");
    }
    return value;
  }

  private DataType convertType(DataTypeXml dataTypeXml) {
    return DataType.valueOf(dataTypeXml.name());
  }

  private Identifier convertQName(QName qName) {
    if (isLocalReference(qName)) {
      return new Identifier(datasetId, qName.getLocalPart());
    } else {
      return getExternalReference(qName);
    }
  }

  private boolean isLocalReference(QName qName) {
    return XMLConstants.DEFAULT_NS_PREFIX.equals(qName.getPrefix())
        || qName.getNamespaceURI().equals(dspl.getTargetNamespace());
  }

  private Identifier getExternalReference(QName qName) {
    String externalDatasetId = namespaceMapping.getDatasetIdForNamespace(qName.getNamespaceURI());
    if (externalDatasetId == null) {
      throw new IllegalArgumentException("Cannot resolve reference to external dataset: "
          + qName.getNamespaceURI());
    }
    return new Identifier(externalDatasetId, qName.getLocalPart());
  }

  @VisibleForTesting
  Info convertInfo(InfoXml infoXml) {
    Info info = new Info();
    if (infoXml.getName() != null) {
      info.setNames(convertStringValues(infoXml.getName().getValue()));
    }
    if (infoXml.getDescription() != null) {
      info.setDescriptions(convertStringValues(infoXml.getDescription().getValue()));
    }
    if (infoXml.getUrl() != null) {
      info.setUrls(convertStringValues(infoXml.getUrl().getValue()));
    }
    return info;
  }

  private Map<String, String> convertStringValues(List<ValueXml> values) {
    ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
    for (ValueXml value : values) {
      String lang = value.getLang() == null ? "en" : value.getLang();
      builder.put(lang, value.getValue());
    }
    return builder.build();
  }

  /**
   * {@link ConceptNode} view of a DSPL {@link ConceptXml}.
   */
  private class ConceptNodeImpl implements ConceptNode {

    private final ConceptXml conceptXml;

    private ConceptNodeImpl(ConceptXml conceptXml) {
      this.conceptXml = Preconditions.checkNotNull(conceptXml);
    }

    private ConceptXml getLocalConcept(String conceptId) {
      for (ConceptXml concept : dspl.getConcepts().getConcept()) {
        if (concept.getId().equals(conceptId)) {
          return concept;
        }
      }
      return null;
    }

    private ConceptNode getConceptNode(QName conceptId) {
      if (isLocalReference(conceptId)) {
        ConceptXml conceptXml = getLocalConcept(conceptId.getLocalPart());
        if (conceptXml != null) {
          return new ConceptNodeImpl(conceptXml);
        }
      } else {
        Identifier externalConceptId = getExternalReference(conceptId);
        if (externalConceptId != null) {
          return datasetResolver.getConceptNode(externalConceptId);
        }
      }
      return null;
    }

    @Override
    public Identifier getId() {
      return new Identifier(datasetId, conceptXml.getId());
    }

    @Override
    public boolean isParent(Identifier possibleParent) {
      if (conceptXml.getExtends() != null) {
        Identifier parentId = convertQName(conceptXml.getExtends());
        return possibleParent.equals(parentId);
      }
      return false;
    }

    @Override
    public ConceptNode getParent() {
      QName parent = conceptXml.getExtends();
      if (parent != null) {
        return getConceptNode(parent);
      }
      return null;
    }

    @Override
    public DataType getType() {
      return conceptXml.getType() == null ? null : convertType(conceptXml.getType().getRef());
    }

    @Override
    public DataType getAttributeType(String attributeId) {
      AttributeXml attribute = getAttribute(attributeId);
      if (attribute != null) {
        // Get type
        if (attribute.getType() != null) {
          return convertType(attribute.getType().getRef());
        }
        if (attribute.getConcept() != null) {
          // Infer type from concept
          ConceptNode conceptNode = getConceptNode(attribute.getConcept());
          if (conceptNode != null) {
            return ConceptInheritenceTraverser.resolveConceptType(conceptNode);
          }
        }
      }
      return null;
    }

    @Override
    public Identifier getConceptReferencedByAttribute(String attributeId) {
      AttributeXml attribute = getAttribute(attributeId);
      if (attribute != null & attribute.getConcept() != null) {
        return convertQName(attribute.getConcept());
      }
      return null;
    }

    private AttributeXml getAttribute(String attributeId) {
      // Find matching attribute
      for (AttributeXml attribute : conceptXml.getAttribute()) {
        String id = null;
        if (attribute.getId() != null) {
          id = attribute.getId();
        } else if (attribute.getConcept() != null) {
          id = attribute.getConcept().getLocalPart();
        }
        if (attributeId.equals(id)) {
          return attribute;
        }
      }
      return null;
    }

    @Override
    public DataType getPropertyType(String propertyId) {
      // Find matching property
      for (ConceptPropertyXml property : conceptXml.getProperty()) {
        String id = null;
        if (property.getId() != null) {
          id = property.getId();
        } else if (property.getConcept() != null) {
          id = property.getConcept().getLocalPart();
        }
        if (propertyId.equals(id)) {
          // Get type
          if (property.getType() != null) {
            return convertType(property.getType().getRef());
          }
          if (property.getConcept() != null) {
            // Infer type from concept
            ConceptNode conceptNode = getConceptNode(property.getConcept());
            if (conceptNode != null) {
              return ConceptInheritenceTraverser.resolveConceptType(conceptNode);
            }
          }
        }
      }
      return null;
    }

    @Override
    public List<ConceptProperty> getProperties() {
      List<ConceptProperty> properties = Lists.newArrayList();
      for (ConceptPropertyXml property : conceptXml.getProperty()) {
        properties.add(convertProperty(property));
      }
      return properties;
    }

    @Override
    public String getTable() {
      if(conceptXml.getTable() != null) {
        return conceptXml.getTable().getRef();
      } else if (conceptXml.getData() != null) {
        return conceptXml.getData().getTable().getRef();
      } else {
        return null;
      }
    }
  }
}
